.NET 10.0 Preview 1 ist am 25. Februar 2025 erschienen. Am 18. März 2024 folgte Preview 2. Seit dem 10. April 2025 gibt es Preview 3 und am 13. Mai 2025 kam Preview 4. Für .NET 10.0 Preview 4 brauchen Sie auf dem Entwicklungsrechner
- das .NET 10.0 SDK [1], das die drei .NET-Laufzeitumgebungen beinhaltet, sowie
- Visual Studio 2022 Version 17.14.
.NET 10.0 SDK kann direkt zusammen mit Visual Studio 2022 Version 17.14 installiert werden.
C# 14.0: Erweiterungsmitglieder (Extension Members)
Die nachträgliche Erweiterbarkeit von Klassen (auch wenn diese bereits anderenorts kompiliert sind, z. B. in den von Microsoft gelieferten Klassen in den .NET-Klassenbibliotheken) um zusätzliche Methoden, gibt es unter dem Namen „Extension Methods“ bereits seit C#-Sprachversion 3.0, die zusammen mit .NET Framework 3.5 im Jahr 2007 erschienen ist. Man kann mit Extension Methods aber lediglich eine Instanzmethode zu bestehenden Klassen ergänzen. So mussten Entwicklerinnen und Entwickler zwangsweise Konstrukte, die vom Namen her eigentlich Properties waren, leidigerweise als Methoden ausdrücken, siehe IsEmptyClassic() in Listing 1.
public static class StringExtensionClassic { public static string TruncateClassic(this string s, int count) { if (s == null) return ""; if (s.Length <= count) return s; return s.Substring(0, count) + "..."; } public static bool IsEmptyClassic(this string s) => String.IsNullOrEmpty(s); }
In der .NET-Klassenbibliothek gibt es aus diesem Grund einige Erweiterungsmethoden, die Namen besitzen, die man intuitiv als Property erwarten würde, z. B.:
- Enumerable.Count()
- Queryable.Count()
- Enumerable.First()
- Enumerable.Last()
In C# 14.0 bietet Microsoft nun mit dem neuen Blockschlüsselwort extension eine verallgemeinerte Möglichkeit der Erweiterung bestehender .NET-Klassen, die „Extension Members“ heißt.
Das Schlüsselwort extension muss Teil einer statischen, nichtgenerischen Klasse auf der obersten Ebene sein (also keine Nested Class). Nach dem Schlüsselwort extension deklariert man den zu erweiternden Typ (Receiver). In Listing 2 ist der Receiver die Klasse System.String (alternativ abgekürzt durch den eingebauten Typ string). Alle Methoden und Properties innerhalb des Extension-Blocks erweitern dann den hier genannten Receiver-Typ. Aktuell kann man in diesen Extension-Blöcken folgende Konstrukte verwenden (Listing 2):
- Instanz-Methoden
- statische Methoden
- Instanz-Properties mit Getter
- statische Properties mit Getter
Eine Klasse darf mehrere Extension-Blöcke sowie zusätzlich auch klassische Extension Methods und andere statische Mitglieder enthalten (Listing 2). Das erlaubt Entwicklerinnen und Entwicklern, eine bestehende Klasse mit Extension Methods um Extension-Blöcke zu erweitern. Es darf auch mehrere Klassen mit Extension-Blöcken für einen Receiver-Typ geben.
public static class MyExtensions { extension(System.String s) { // Erweitern um eine Instanz-Methode public string Truncate(int count) { if (s == null) return ""; if (s.Length <= count) return s; return s.Substring(0, count) + string.Dots; } // Erweitern um eine Instanz-Eigenschaft public bool IsEmpty => String.IsNullOrEmpty(s); // Erweitern um eine Instanz-Eigenschaft mit Getter und Setter public int Size { get { return s.Length; } set { if (value < s.Length) s = s.Substring(0, value); if (value > s.Length) s = s + new string('.', value - s.Length); // Neuzuweisung geht nicht !!! } } // Erweitern um eine statische Methode public static string Create(int count, char c = '.') { return new string(c, count); } // Erweitern um eine statische Instanz-Eigenschaft public static string Dots => "..."; } // Es darf auch eine Extension für einen anderen Typen geben extension(List<int> source) { public List<int> WhereGreaterThan(int threshold) => source.Where(x => x > threshold).ToList(); public bool IsEmpty => !source.Any(); // Erweitern um eine Instanz-Eigenschaft mit Getter und Setter public int Size { get { return source.Count; } set { if (value < source.Count) source = source.Take(value).ToList(); if (value > source.Count) source.AddRange(Enumerable.Repeat(0, value - source.Count)); } } } // Es kann auch eine klassische Extension Method in der gleichen Kasse geben public static string TruncateClassic2(this string s, int count) { if (s == null) return ""; if (s.Length <= count) return s; return s.Substring(0, count) + "..."; } // Es kann auch andere statische Methoden in der gleichen Klasse geben public static Version ExtensionVersion => new Version(1, 0, 0); }
Zu beachten ist:
- Man kann mit Extension Members auch das Gleiche tun, was Extension Methods bisher konnten. Entwicklerinnen und Entwickler haben nun die Wahl, Extension Methods für alte oder die neue Syntax zu verwenden. Microsoft nennt die alte Syntax zur Abgrenzung „this-parameter extension methods“. Visual Studio wird Refactoring-Methoden zur Umwandlung zwischen beiden Syntaxformen anbieten.
- Beim Versuch, in einem Extension-Block ein Field zu geben, meckert der Compiler leider mit der unzutreffenden Meldung „Extension declarations can include only methods or properties“.
- Die Syntax für Extensions, die Microsoft für C# 13.0 in Arbeit hatte (public implicit extension Name for Typ), wurde verworfen.
- Microsoft plant in kommenden Preview-Versionen weitere Konstrukte in Extension-Blöcken zu erlauben, z. B. Konstruktoren.
- Listing 3 zeigt den Aufruf der alten und neuen Erweiterungen.
public class CS14_ExtensionDemo { public void Run() { CUI.Demo(nameof(CS14_ExtensionDemo) + ": String"); string s1old = "Hallo Holger"; string s1oldtruncated = s1old.TruncateClassic(5); Console.WriteLine(s1oldtruncated); // Hello... string s2old = null; Console.WriteLine(s2old.IsEmptyClassic()); // true string s1 = "Hallo Holger"; string s1truncated = s1.Truncate(5); Console.WriteLine(s1truncated); // Hello... // Das geht nicht, weil das Size Property versucht, die Zeichenkette neu zuzuweisen! s1.Size = 5; Console.WriteLine(s1); // Hallo Holger statt wie erwartet Hallo string s2 = null; Console.WriteLine(s2.IsEmpty); // true string s3 = string.Create(5, '#'); Console.WriteLine(s3); // "#####" CUI.Demo(nameof(CS14_ExtensionDemo) + ": Collection"); var list = new List<int> { 1, 2, 3, 4, 5 }; var large = list.WhereGreaterThan(3); Console.WriteLine(large.IsEmpty); if (large.IsEmpty) { Console.WriteLine("Keine Zahlen größer als 3!"); } else { Console.WriteLine(large.Count + " Zahlen sind größer als 3!"); } // Das klappt: Die Liste wird auf 10 Elemente aufgefüllt list.Size = 10; foreach (var x in list) { Console.WriteLine(x); } } }
C# 14.0: Null-Conditional Assignment mit ?.
Ein weiteres sehr hilfreiches neues Sprachkonstrukt in C# 14.0 nennt Microsoft das „Null-Conditional Assignment“. Damit können Entwicklerinnen und Entwickler eine Zuweisung an eine Eigenschaft machen ohne vorher zu prüfen, ob das Objekt null ist. Anstelle von
Website aktuelleDOTNETWebsite = Website.Load(123); if (aktuelleDOTNETWebsite!= null) { // Aktualisieren der URL aktuelleDOTNETWebsite.Url = "https://www.dotnet10.de"; }
darf man nun verkürzt mit dem Fragezeichen vor dem Punkt (?.) schreiben:
aktuelleDOTNETWebsite?.Url = "https://www.dotnet10.de";
Dies führt zur Laufzeit zu keinem Fehler. Allerdings passiert auch rein gar nichts, falls die Variable aktuelleDOTNETWebsite den Wert null besitzt.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
.NET SDK: Kompilieren und Starten einzelner C#-Dateien (C# Scripting)
Seit .NET 10.0 Preview 4 können Entwicklerinnen und Entwickler einzelne C#-Dateien direkt übersetzen und starten – ohne dass es eine Projektdatei geben muss. Das ist möglich mit dem .NET-SDK-CLI-Befehl dotnet run (Abb. 1).

Abb. 1: Start einer eigenständigen C#-Datei mit dotnet run
Damit kann C# nun auch als Skriptsprache zum Einsatz kommen. Es gab dafür aber schon vorher Ansätze außerhalb von Microsoft:
- CS-Script [2]
- DOTNET-Script [3]
- Cake [4]
- DOTNET Scripting Host (DSH) [5]
Auch der klassische Stil mit class Program und Main()-Methode ist möglich in den eigenständigen C#-Dateien:
class Program { static void Main(string[] args) { Console.WriteLine("Hallo von " + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); } }
Für Informationen, die üblicherweise in einer Projektdatei liegen, hat Microsoft eine eigene Syntax beginnend mit der Raute # (Präprozessordirektiven) eingeführt:
- Festlegung des SDK: #:sdk Microsoft.NET.Sdk.Web
- Referenz auf ein NuGet-Paket: #:package [email protected].*
- Build-Eigenschaften, z. B. Versionsnummer: #:property Version 1.1.2
Listing 4 zeigt ein Beispiel mit zwei NuGet-Paketen.
#:package [email protected] #:package [email protected].* #:property LangVersion preview #:property Version 1.1.2 using Spectre.Console; using Humanizer; var title = "C# Script " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version + " mit " + System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; var panel = new Panel(title); AnsiConsole.Write(panel); Console.WriteLine(); var d = new Data { Version = "9.0", Release = "2024-12-3" }; var dotNet9Released = DateTimeOffset.Parse(d.Release); var since = DateTimeOffset.Now - dotNet9Released; Console.WriteLine($"It has been {since.Humanize()} since .NET {d.Version} was released."); class Data { public string Version { get; set; } public string Release { get; set; } }
Das nächste Beispiel in Listing 5 und Abbildung 2 zeigt ein ASP.NET Core Minimal WebAPI als eigenständige C#-Datei.
#:sdk Microsoft.NET.Sdk.Web #:package Microsoft.AspNetCore.OpenApi@10.*-* #:package [email protected] #:property Version 1.1.4 using Humanizer; using Microsoft.OpenApi; // Webserver einrichten var builder = WebApplication.CreateBuilder(); builder.Services.AddOpenApi(); var app = builder.Build(); app.MapOpenApi(); // http://localhost:5000/openapi/v1.json app.MapGet("/", () => { // Daten für Operation var d = new Data { Version = "9.0", Release = "2024-12-3" }; var dotNet9Released = DateTimeOffset.Parse(d.Release); var since = DateTimeOffset.Now - dotNet9Released; return $"It has been {since.Humanize()} since .NET {d.Version} was released."; }); app.Run(); class Data { public string Version { get; set; } public string Release { get; set; } }

Abb. 2: Start und Ausgabe des Webservers
.NET SDK: Umwandeln eigenständiger C#-Dateien in C#-Projekte
Wenn die Anforderungen höher werden, sind C#-Skripte keine Sackgasse. Man kann per Kommandozeilenbefehl aus einem C#-Skript ein C#-Projekt machen (Abb. 3):
dotnet project convert .\WebserverStandalone.cs

Abb. 3: Umwandeln einer eigenständigen C#-Skriptdatei in ein C#-Projekt
Dabei werden ein neuer Ordner und eine Projektdatei angelegt, wobei letztere die Präprozessorinformationen aus der C#-Datei übernimmt (Abb. 4).

Abb. 4: Das eigenständige C#-Projekt
Basisklassenbibliothek: asynchrone ZIP-Operationen
ZIP-Komprimierung gibt es in der .NET-Basisklassenbibliothek seit dem klassischen .NET Framework 4.5 und im modernen .NET seit Version .NET Core 1.0. Seit .NET 10.0 Preview 4 gibt es nun in den Klassen System.IO.Compression.ZipFile, System.IO.Compression.ZipArchive und System.IO.Compression.ZipEntry asynchrone Pendants zu bestehenden synchronen Methoden, z. B. ExtractToDirectoryAsync(), ExtractToFileAsync(), CreateFromDirectoryAsync(), OpenAsync(), OpenReadAsync(), CreateAsync() und CreateEntryFromFileAsync(). Listing 6 zeigt mehrere Beispiele für asynchrone ZIP-Operationen.
using System.IO.Compression; using System.Text; using ITVisions; namespace NET10_Console; internal class FCL10_Zip { private const string ArchiveFileName = @"t:\CTempArchive.zip"; private const string SourceDirectoryName = @"c:\temp"; private const string DestinationDirectoryName = @"t:\CTempArchiveExtract"; private const string TempFileName = @"t:\tempfile.pdf"; public async Task Run() { CUI.Demo(nameof(FCL10_Zip)); // Prüfe, ob die Datei existiert if (File.Exists(ArchiveFileName)) File.Delete(ArchiveFileName); // Create a Zip archive await ZipFile.CreateFromDirectoryAsync(SourceDirectoryName, ArchiveFileName, CompressionLevel.SmallestSize, includeBaseDirectory: true, entryNameEncoding: Encoding.UTF8); // Prüfe, ob die Datei existiert if (File.Exists(ArchiveFileName)) CUI.Green("ZIP-Datei erstellt: " + ArchiveFileName); else CUI.Red("ZIP-Datei nicht erstellt: " + ArchiveFileName); // Extract a Zip archive await ZipFile.ExtractToDirectoryAsync(ArchiveFileName, DestinationDirectoryName, overwriteFiles: true); // Prüfe, ob das Verzeichnis existiert if (Directory.Exists(DestinationDirectoryName)) CUI.Green("Verzeichnis extrahiert: " + DestinationDirectoryName); else CUI.Red("Verzeichnis nicht extrahiert: " + DestinationDirectoryName); #region Lesen und Schreiben von Einträgen in eine ZIP-Datei using (var archiveStream = new FileStream(ArchiveFileName, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { await using (ZipArchive a = await ZipArchive.CreateAsync(archiveStream, ZipArchiveMode.Update, leaveOpen: false, entryNameEncoding: Encoding.UTF8)) { // Suche die erste PDF-Datei in a.Entries var pdfFileEntry = a.Entries.Where(x => x.Name.EndsWith(".pdf")).FirstOrDefault(); if (pdfFileEntry != null) { await pdfFileEntry.ExtractToFileAsync(destinationFileName: TempFileName, overwrite: true); await using Stream entryStream = await pdfFileEntry.OpenAsync(); ZipArchiveEntry createdEntry = await a.CreateEntryFromFileAsync(sourceFileName: TempFileName, entryName: "Doppelt_" + pdfFileEntry.Name); CUI.Green("Erste PDF-Datei wurde verdoppelt: " + pdfFileEntry.Name); } } } #endregion } }
Basisklassenbibliothek: mehr Kontrolle beim Zertifikatsexport mit X509Certificate.ExportPkcs12()
Eine Verbesserung für den Zertifikatsexport stand schon in den Release Notes zu .NET 10.0 Preview 2 [6], funktionierte dort aber nicht. Seit .NET 10.0 Preview 3 kann man tatsächlich Zertifikate mit AES-Verschlüsselung und Hashing via SHA-2-256 exportieren – mit der neuen Methode ExportPkcs12() als Ergänzung zur bestehenden Methode Export():
byte[] pfxData = cert.ExportPkcs12(Pkcs12ExportPbeParameters.Pbes2Aes256Sha256, password);
Die bisherige Methode Export() verwendet noch veraltete Algorithmen (3DES-Verschüsselung und SHA-1-Hashing). Die alten Verfahren (3DES/SHA-1) aus Export() sind aber auch über die neue Methode möglich, wenn man es wirklich will:
byte[] pfxData = cert.ExportPkcs12(Pkcs12ExportPbeParameters.Pkcs12TripleDesSha1, password);
Listing 7 zeigt ein Beispiel für die Erstellung und den Export eines Zertifikats.
string certPath = "meinZertifikat.pfx"; string password = "meinPasswort"; // Passwort zum Schutz des Zertifikats // Selbstsigniertes Zertifikat erstellen using (var rsa = RSA.Create(2048)) { var request = new CertificateRequest( "CN=TestZertifikat", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // Zertifikat für 1 Jahr gültig machen var cert = request.CreateSelfSigned( DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); // ALT: Zertifikat mit 3DES-Verschüsselung und SHA1-Hashing exportieren //byte[] pfxData = cert.Export(X509ContentType.Pkcs12, password); // NEU: Zertifikat als AES mit SHA-2-256 exportieren byte[] pfxData = cert.ExportPkcs12(Pkcs12ExportPbeParameters.Pbes2Aes256Sha256, password); // NEUE Methode, aber alte Sicherheitsverfahren: 3DES mit SHA-1 //byte[] pfxData = cert.ExportPkcs12(Pkcs12ExportPbeParameters.Pkcs12TripleDesSha1, password); // PFX-Datei speichern File.WriteAllBytes(certPath, pfxData); Console.WriteLine($"Test-Zertifikat erstellt und gespeichert unter: {certPath}"); }
System.Text.Json: JSON-Patch-Unterstützung
System.Text.Json beherrscht nun den JSON-Patch-Standard nach RFC 6902. JSON Patch ist ein standardisiertes Format (definiert in RFC 6902), um Änderungen an JSON-Dokumenten zu beschreiben. Es ermöglicht die Übertragung von Änderungen an einem JSON-Dokument in Form einer Liste von Anweisungen (Operationen). JSON Patch wird häufig in WebAPIs eingesetzt.
Bisher musste man in ASP.NET-Core-basierten WebAPIs für JSON Patch die ältere Community-Bibliothek Newtonsoft.Json einsetzen. Nun geht es auch mit der neueren Microsoft-Bibliothek System.Text.Json [7].
Für JSON Patch mit System.Text.Json gibt es eine Erweiterung für System.Text.Json in Form des neuen NuGet-Pakets Microsoft.AspNetCore.JsonPatch.SystemTextJson, das erstmalig mit .NET 10.0 Preview 4 erschienen ist [8].
Trotz des “AspNetCore” im Namen funktioniert dieses Zusatzpaket auch außerhalb von ASP.NET Core, wie Listing 8 beweist! Allerdings funktioniert das neue Paket nicht mit dem NativeAOT-Compiler.
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public List<PhoneNumber> PhoneNumbers { get; set; } = new(); public Address Address { get; set; } } public class Address { public string Street { get; set; } public string City { get; set; } public string State { get; set; } public string ZipCode { get; set; } } public enum PhoneNumberType { Mobile, Home, Work, Other } public class PhoneNumber { public string? Number { get; set; } public PhoneNumberType Type { get; set; } } public void JSONPatchDemo() { // Originalobjekt var person = new Person { FirstName = "Holger", LastName = "Schwichtenberg", Email = "[email protected]", PhoneNumbers = [new PhoneNumber() { Number = "0201 649590-0", Type = PhoneNumberType.Work }], Address = new Address { Street = "Fahrenberg 40b", City = "Essen", State = "NRW" } }; CUI.H2("Ausgabe des Objekts in seinem alten Zustand"); CUI.Print(ITVisions.ObjectDumper.Dump(person)); // JSON Patch-Document string jsonPatch = """ [ { "op": "replace", "path": "/FirstName", "value": "Dr. Holger" }, { "op": "remove", "path": "/Email"}, { "op": "add", "path": "/Address/ZipCode", "value": "45257" }, { "op": "add", "path": "/PhoneNumbers/-", "value": { "Number": "0201 649590-40" } } ] """; // JSON Patch-Document laden JsonPatchDocument<Person>? patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch); // JSON Patch-Document anwenden auf das Person-Objekt patchDoc!.ApplyTo(person); CUI.H2("Ausgabe des Objekts in seinem neuen Zustand"); CUI.Print(ITVisions.ObjectDumper.Dump(person)); }
Microsoft verspricht in der neuen Implementierung „eine verbesserte Leistung und weniger Speichernutzung im Vergleich zu der existierenden Implementierung in Newtonsoft.Json“ [9] in Verbindung mit einem ähnlichen Design wie bei Newtonsoft.Json, einschließlich der Aktualisierung eingebetteter Objekte und Arrays. Bisher nicht verfügbar ist JSON Patch für dynamische Objekte, weil Newtonsoft.Json dafür Reflection einsetzt, System.Text.Json soll aber auch mit dem NativeAOT-Compiler funktionieren.
Im NuGet-Paket Microsoft.AspNetCore.JsonPatch.SystemTextJson gibt es eine Klasse Microsoft.AspNetCore.JsonPatch.SystemTextJson.JsonPatchDocument<T> mit einer Methode ApplyTo (obj), die eine JSON-Patch-Operation auf das übergebene Objekt anwendet (Listing 8). Das resultierende JSON-Dokument für die Person nach Anwendung des JSON-Patch-Dokuments sieht dann aus wie in Abbildung 5.

Abb. 5: Ausgabe des Listings 8
Alternativ kann man das Objekt natürlich auch als JSON ausgeben (Abb. 6). Das ist nur in dem obigen Listing nicht passiert, weil nicht der Eindruck entstehen soll, man müsse das Objekt als JSON weiterverwenden.

Abb. 6: Ausgabe des geänderten Objekts im JSON-Format
Mit JSON Patch kann man auch Werte in einem Objekt testen (Listing 9).
CUI.H2("Testen mit JSON Patch-Dokument"); string jsonPatchTest = """ [ { "op": "test", "path": "/FirstName", "value": "Dr. Holger" }, { "op": "test", "path": "/Email", "value": null}, { "op": "test", "path": "/Address/ZipCode", "value": "45257" } ] """; // JSON Patch-Document laden JsonPatchDocument<Person>? patchTestDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatchTest); // JSON Patch-Document anwenden auf das Person-Objekt patchTestDoc!.ApplyTo(person, patchError => CUI.PrintError(patchError.ErrorMessage));
In diesem Fall gibt es keine Fehlerausgabe. Wenn man das Dokument aber verändert (Listing 10), bekommt man drei Fehlertexte (Abb. 7).
CUI.H2("Testen mit JSON Patch-Dokument"); string jsonPatchTest = """ [ { "op": "test", "path": "/FirstName", "value": "Dr.Holger" }, { "op": "test", "path": "/Email", "value": ""}, { "op": "test", "path": "/Address/ZipCode", "value": "45251" } ] """;

Abb. 7: Fehlertexte
Dabei ist die Fehlermeldung bei Email nicht gut, denn sie unterscheidet nicht zwischen Leerstring und Null. Im Objekt steht null, getestet wird auf Leerstring. In der Fehlermeldung steht in beiden Fällen ‘ ‘.
Entity Framework Core: verbesserte Suche mit CosmosDB
Entity Framework Core 10.0 unterstützt seit Preview 4 in Verbindung mit Azure CosmosDB zwei neue Suchmodi, die es dort seit März 2025 als Vorschauversion gibt: Volltextsuche [10] und hybride Suche, die die Vektorsuche und Volltextsuche mit einer Reciprocal Rank Fusion (RRF) Function kombiniert [11]. Microsoft hat zudem angekündigt [12], dass die in Entity-Framework-Core-Version 9.0 eingeführte Vektorsuche mit CosmosDB nun stabil ist.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
ASP.NET Core: OpenAPI Specification (OAS) in der WebAPI-AOT-Projektvorlage
Bisher war die Metadatengenerierung mit OpenAPI Specification (OAS) nur in der Projektvorlage ASP.NET Core Web API (Kurzname: webapi) aktiv, die keine Kompilierung mit dem NativeAOT-Compiler erlaubt. In der Projektvorlage ASP.NET Core Web API (nativeAOT) (Kurzname: webapiaot) mit aktivierter NativeAOT-Kompilierung war OAS zwar integrierbar, aber das mussten Entwicklerinnen und Entwickler manuell vornehmen.
Seit .NET 10.0 Preview 4 ist in der Projektvorlage ASP.NET Core Web API (nativeAOT) nun die Metadatengenerierung mit OAS im Standard aktiviert (Abb. 8). Mit dem Befehl
dotnet new webapiaot --name ITVisionsWebAPI
entsteht ein Projekt, das das NuGet-Paket Microsoft.AspNetCore.OpenApi referenziert und die OpenAPI-Unterstützung in der Program.cs mit
builder.Services.AddOpenApi();
und
app.MapOpenApi();
aktiviert.
In der Projektvorlage webapiaot können Entwicklerinnen und Entwickler die OpenAPI-Features wahlweise auch mit dem Parameter –no-openapi deaktivieren:
dotnet new webapiaot --name ITVisionsWebAPI --no-openapi

Abb. 8: Die neue Option für WebAPI-AOT-Projekte in Visual Studio
Mit aktiviertem OpenAPI-Support wird dann dieses NuGet-Paket eingebunden:
<PackageReference Include="Microsoft.AspNetCore.OpenApi" … />
In Program.cs steht dann:
builder.Services.AddOpenApi(); ... if (app.Environment.IsDevelopment()) { app.MapOpenApi(); }
Der Standard-URL des OpenAPI-Dokuments ist dann: https://localhost:PORT/openapi/v1.json.
ASP.NET Core: OpenAPI-Transformer für Minimal WebAPIs
Seit .NET 10.0 Preview 3 gibt es eine neue Methode AddOpenApiOperationTransformer(), mit der Entwicklerinnen und Entwickler das generierte OpenAPI-Dokument verändern können. Beispiel: Eine Operation /checkwebsite liefert normalerweise HTTP-Statuscode 200 mit der Beschreibung „OK“ zurück (Abb. 9).

Abb. 9: Ausschnitt aus dem OpenAPI-Dokument vor Veränderung des Antwortbeschreibungstexts und der Parameterliste
Dieser Text kann verändert werden mit Aufruf von AddOpenApiOperationTransformer(). Ebenso kann ein Parameter ergänzt werden, z. B. ein zusätzlicher Wert, der im Header des HTTP-Requests übergeben werden muss (Listing 11), (Abb. 10).
app.MapGet("/checkwebsite", Operations.CheckWebsite) .WithName("checkwebsite") .AddOpenApiOperationTransformer((operation, ContextBoundObject, ct) => { // Ergänzen eines Parameters, der im Header übergeben werden muss operation.Parameters.Add(new OpenApiParameter { Name = "Customer-GUID", In = ParameterLocation.Header, Description = "Your Customer GUID for Authentication", Required = true, Schema = new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid" } }); // Änderung des Beschreibungstextes für Statuscode 200 operation.Responses?["200"].Description = "Die Prüfung war erfolgreich"; return Task.CompletedTask; });

Abb. 10: Ausschnitt aus dem OpenAPI-Dokument nach Veränderung des Antwortbeschreibungstextes
ASP.NET Core: Validierung für ASP.NET Core Minimal WebAPIs
ASP.NET Core Minimal WebAPIs beherrschen seit .NET 10.0 Preview 3 auch die Parametervalidierung mit Data Annotations. Das konnten bisher nur die Controller-basierten WebAPIs [13].
Für Minimal WebAPIs müssen Entwicklerinnen und Entwickler das Validierungsfeature allerdings erst in der Projektdatei aktivieren mit
<PropertyGroup> <!-- Enable the generation of interceptors for the validation attributes --> <InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces> </PropertyGroup>
und in der Startdatei mit
builder.Services.AddValidation();
Dann kann man auch in Minimal WebAPIs mit den bekannten Validierungsannotationen wie [MinLength], [MaxLength], [Range], [Url], [EmailAddress], [Phone], [CreditCard], [RegularExpression] u. v. m. [14] arbeiten, z. B.
app.MapGet("/checkwebsite", ([MinLength(5)] string name, [Url] string url) => WebsiteChecker.CheckWebsite(name, url));
Hier führt nun der HTTP-Aufruf https://Server:Port/checkwebsite?name=ITVisions&url=www.IT-Visions.de zur Laufzeit zur Rückgabe eines JSON-Dokuments zu zwei Validierungsfehlern (Abb. 11). Seit Preview 4 funktioniert das auch, wenn die Parameter ein Record-Typ sind.

Abb. 11: Beide Parameter beim Aufruf dieses ASP.NET Core Minimal WebAPI entsprachen hier nicht den Validierungsregeln
Blazor: deklarativer Persistent Component State
Microsoft Webframework ASP.NET Core erlaubt bei Blazor Server und Blazor WebAssembly bisher schon die Übergabe von Daten zwischen dem Prerendering und dem Hauptrendering via JSON-Anhang in HTML-Dokumenten. Dieser Persistent Component State war bisher aber recht aufwendig für die Entwicklerinnen und Entwickler in der Realisierung, denn man musste zunächst ein Objekt per Dependency Injection bekommen
@inject PersistentComponentState ApplicationState
und dann den Zustand als Objekt dort ablegen
private PersistingComponentStateSubscription? persistingSubscription; persistingSubscription = ApplicationState.RegisterOnPersisting(() => { ApplicationState.PersistAsJson("name", daten); return Task.CompletedTask; });
und jeweils vor der Nutzung wieder explizit herausholen:
if (ApplicationState.TryTakeFromJson<Typ>("name", out var daten)) { Daten = daten; }
Das hat Microsoft in .NET 10.0 Preview 3 mit dem deklarativen Persistent Component State radikal vereinfacht. Entwicklerinnen und Entwickler müssen jetzt nur noch ein Property mit der Annotation [SupplyParameterFromPersistentComponentState] versehen und sich sonst um nichts kümmern:
[SupplyParameterFromPersistentComponentState] public Daten? daten { get; set; }
Im Beispiel in Listing 12 sehen Sie auskommentiert die alte Variante im Vergleich zur neuen Variante.
@page "/State" @using BO.WWWings @* @inject PersistentComponentState ApplicationState *@ <PageTitle>Counter</PageTitle> <h1>Flight-Counter <span class="badge bg-warning">@this.RendererInfo.Name</span></h1> <p role="status" title="@pageState.LastChange">Current count: @pageState.CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">+</button> <button class="btn btn-primary" @onclick="DecrementCount">-</button> @if (this.pageState?.FlightSet != null) { <hr noshade> <ol> @foreach (Flight f in this.pageState.FlightSet.Take(this.pageState.CurrentCount)) { <li>Flug #@f.FlightNo (@f.FlightDate.ToShortDateString()) @f.Departure -> @f.Destination </li> } </ol> } @code { [SupplyParameterFromPersistentComponentState] public PageState pageState { get; set; } = null; private void IncrementCount() { pageState.CurrentCount++; } private void DecrementCount() { if (pageState.CurrentCount > 0) pageState.CurrentCount--; } // ----------------------------- // private PersistingComponentStateSubscription? persistingSubscription; protected override void OnInitialized() { // persistingSubscription = ApplicationState.RegisterOnPersisting(() => // { // ApplicationState.PersistAsJson(nameof(PageState), pageState); // return Task.CompletedTask; // }); if (pageState == null) { DA.WWWings.WwwingsV1EnContext ctx = new(); pageState = new() { FlightSet = ctx.Flights.Take(100).ToList(), CurrentCount = 10, Created = DateTime.Now }; } // if (ApplicationState.TryTakeFromJson<PageState>(nameof(PageState), out PageState daten)) // { // pageState = daten; // } } public class PageState { public DateTime Created { get; set; } public DateTime LastChange { get; set; } public int _CurrentCount; public int CurrentCount { get { return _CurrentCount; } set { _CurrentCount = value; LastChange = DateTime.Now; } } public List<Flight> FlightSet { get; set; } } }
Blazor: Erweiterungen der Klasse NavigationManager
Die in Blazor eingebauten Implementierungen der abstrakten Basisklasse Microsoft.AspNetCore.Components.NavigationManager haben in Blazor 10.0 Preview 4 zwei neue Mitglieder erhalten: NotFound() und OnNotFound().
Die neue Methode NotFound() signalisiert Blazor, dass eine angeforderte Ressource nicht vorhanden ist. Beim statischen Server-side Rendering bekommt der Webbrowser dann eine HTTP-Antwort mit Statuscode 404. Beim interaktiven Rendering (Blazor Server, Blazor WebAssembly, Blazor Hybrid) wird gerendert, was im Router bei <NotFound> definiert ist.
OnNotFound() ist ein Ereignis, das von Blazor ausgelöst wird, wenn die Methode NotFound() aufgerufen wird.
NavigationManager.NavigateTo() funktioniert nun auch beim statischen Server-side Rendering. Bisher lieferte dies immer den Laufzeitfehler NavigationException. Für Anwendungen, die diesen Fehler erwarten, hat Microsoft einen Schalter eingebaut, der das bisherige Verhalten zurückholt:
AppContext.SetSwitch("Microsoft.AspNetCore.Components.Endpoints.NavigationManager.EnableThrowNavigationException", isEnabled: true);
Blazor: Fingerprinting für Blazor WebAssembly Standalone
Das bereits in Blazor 9.0 eingeführte Fingerprinting (Anhängen eines Hashwerts auf Basis des Dateiinhalts an den Dateinamen, was vermeidet, dass Webbrowser alte Versionen aus dem Browsercache verwenden) funktioniert nun auch für die in Blazor eingebaute JavaScript-Datei in den sogenannten „Standalone Blazor WebAssembly“-Projekten, die nicht in ASP.NET Core betrieben werden, sondern auf einem statischen Webserver gehostet sein können.
Man verwendet dazu man in den Projekteinstellung
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
und im HTML-Kopfbereich in der index.html:
<link rel="preload" id="webassembly" /> <script type="importmap"></script>
Das <script>-Tag ist dann
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
Diese Verbesserung ist seit .NET 10.0 Preview 4 auch in der Projektvorlage blazorwasm umgesetzt:
dotnet new blazorwasm -n ITVisionsDemo
Die index.html-Datei sieht dort so aus wie in Listing 13 gezeigt.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>NET10_BlazorWasmStandalone</title> <base href="/" /> <link rel="preload" id="webassembly" /> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="css/app.css" /> <link rel="icon" type="image/png" href="favicon.png" /> <link href="NET10_BlazorWasmStandalone.styles.css" rel="stylesheet" /> <link href="manifest.webmanifest" rel="manifest" /> <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> <link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" /> < script type="importmap"></script> </head> <body> <div id="app"> <svg class="loading-progress"> <circle r="40%" cx="50%" cy="50%" /> <circle r="40%" cx="50%" cy="50%" /> </svg> <div class="loading-progress-text"></div> </div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="." class="reload">Reload</a> <span class="dismiss">🗙</span> </div> < script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> < script>navigator.serviceWorker.register('service-worker.js');</script> </body> </html>
Der Build-Prozess sorgt dafür, dass einige Ersetzungen stattfinden (siehe grüne Rahmen in den Abbildungen 12 und 13).

Abb. 12: Ersetzungen in der index.html durch den Build-Prozess (Teil 1)

Abb. 13: Ersetzungen in der index.html durch den Build-Prozess (Teil 2)
Wichtig: In bestehenden Projekten müssen Entwicklerinnen und Entwickler die Änderungen manuell vornehmen, um die Optimierungen zu nutzen.
Blazor: Umgebungen für Blazor WebAssembly
In Blazor-WebAssembly-Anwendungen mussten Entwicklerinnen und Entwickler die Umgebungsdefinition (Development, Staging, Production) bisher per HTTP-Header vornehmen [15]. Nun nimmt Microsoft beim Kompilieren von Blazor-WebAssembly-Anwendungen automatisch folgende Umgebungen an:
- bei dotnet build: Development
- bei dotnet publish: Production
Entwicklerinnen und Entwickler können die Umgebungsart auch per Projekteinstellung in der Projektdatei (.csproj) explizit setzen, z. B. für die Umgebung Staging:
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Blazor: Response Streaming im HttpClient in Blazor WebAssembly
In Blazor-WebAssembly-basierten Anwendungen ist in der Klasse System.Net.Http.HttpClient im Standard nun das Response Streaming aktiv, um die Leistung zu erhöhen und den Speicherbedarf zu verringen (Listing 14, Abb. 14). Zuvor mussten Entwicklerinnen und Entwickler das Response Streaming manuell aktivieren mit
request.SetBrowserResponseStreamingEnabled(true);
@page "/HttpStreaming" @using Microsoft.AspNetCore.Components.WebAssembly.Http @using System.Net.Http @using System.Diagnostics @inject HttpClient HttpClient <h1>HTTP Response Streaming</h1> <button type="button" class="btn btn-primary" @onclick="Get">Laden</button> <button type="button" class="btn btn-warning" @onclick="Stop">Abbrechen</button> <input type="checkbox" @bind="streamingEnabled" /> Response Streaming aktivieren <hr /> <p>Status: @status</p> <p>Response Streaming aktiv: @streamingEnabled</p> <p>Stream-Type: @streamType</p> <p>Bytes gelesen: @byteCount</p> <p>Dauer: @sw.ElapsedMilliseconds ms</p> @code { int byteCount; string status = ""; string streamType = "Unknown"; bool streamingEnabled = false; // Set to true to enable streaming Stopwatch sw = new Stopwatch(); CancellationTokenSource cts; // Laden async Task Get() { status = "Lade..."; sw.Reset(); sw.Start(); cts = new CancellationTokenSource(); using var request = new HttpRequestMessage(HttpMethod.Get, "https://www.it-visions.de/produkte/pdf/www.IT-Visions.de_Firmenbrosch%C3%BCre.pdf"); if (streamingEnabled) request.SetBrowserResponseStreamingEnabled(true); else request.SetBrowserResponseStreamingEnabled(false); // Beim Streaming; HttpCompletionOption.ResponseHeadersRead / Der Standardwert ist ResponseContentRead, was bedeutet, dass der Vorgang erst abgeschlossen werden soll, nachdem die gesamte Antwort, einschließlich des Inhalts, aus dem Socket gelesen wurde. using var response = await HttpClient.SendAsync(request, streamingEnabled ? HttpCompletionOption.ResponseHeadersRead : HttpCompletionOption.ResponseContentRead); using Stream stream = await response.Content.ReadAsStreamAsync(); streamType = stream.GetType().FullName; // Get the type of the stream: System.IO.MemoryStream oder System.Net.Http.HttpConnection+ContentLengthReadStream // Blockweises Lesen des Streams var bytes = new byte[10000]; while (!cts.Token.IsCancellationRequested) { var read = await stream.ReadAsync(bytes, cts.Token); if (read == 0) // Ende des Streams erreicht { sw.Stop(); status = "Fertig!"; return; } byteCount += read; // UI-Update StateHasChanged(); await Task.Delay(1); } } // Abbrechen void Stop() { sw.Stop(); cts?.Cancel(); } }

Abb. 14: Screenshot des Beispiels aus Listing 14 unter .NET 9.0
In .NET 10.0 ist das Response Streaming nun automatisch aktiv und der Stream-Typ ist ein anderer (Abb. 15).

Abb. 15: Screenshot des Beispiels aus Listing 14 unter .NET 10.0
Durch das im Standard aktive Response Streaming sind allerdings nur noch asynchrone Operationen und keine synchronen Operationen mehr möglich. Ein Aufruf der synchronen Send()-Methode HttpClient.Send(request) führt dann zum Laufzeitfehler „Operation is not supported on this platform.“ Diese Verhaltensänderung gehört zu den Breaking Changes in .NET 10.0 [16].
Das Response Streaming im HttpClient ist in .NET 10.0 deaktivierbar mit:
request.SetBrowserResponseStreamingEnabled(false);
Alternativ ist die Deaktivierung global in der Projektdatei möglich:
<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
Blazor: neue Interoperabilitätsmethoden zwischen C# und JavaScript
In Blazor erweitert Microsoft die Schnittstellen für die Interoperabilität zwischen C# und JavaScript (IJSRuntime, IJSInProcessRuntime, IJSObjectReference und IJSInProcessObjectReference) um neue Mitglieder, um eine Instanz von einem JavaScript-Objekt zu erzeugen und dabei Konstruktorparameter zu übergeben (InvokeNew() und InvokeNewAsync()) sowie um Variablen oder Objekteigenschaften zu lesen (GetValue() und GetValueAsync()) bzw. zu setzen (SetValue()und SetValueAsync()).
Bisher konnte man von C# aus lediglich Funktionen in JavaScript mit Invoke(), InvokeVoid(), InvokeAsync() und InvokeVoidAsync() aufrufen. Die neuen Interoperabilitätsmethoden vermeiden in einigen Fällen, dass man eine JavaScript-Wrapper-Funktion schreiben muss; stattdessen kann man nun direkt Objekte erzeugen und Werte lesen/schreiben. Ein Beispiel sieht man in den Listings 15 und 16 sowie Abbildung 16.
window.JSUtil = class { constructor(text) { // zwei Eigenschaften der JavaScript-Klasse this.text = text; this.caseSensitive = false; } // Hole Textlänge getTextLength() { return this.text.length; } // Prüfe, ob Text in der aktuellen URL vorkommt containsTextInUrl() { const url = window.location.href; if (this.caseSensitive) return url.includes(this.text); else return url.toLowerCase().includes(this.text.toLowerCase()); } }
@inject IJSRuntime JSRuntime @page "/interop" @using ITVisions.Blazor @inject BlazorUtil util < script src="JSUtil.js"></script> <h3>Blazor 10.0 C#/JavaScript-Interop</h3> <hr noshade /> <button @onclick="Aktion">JavaScript aufrufen</button> <br /> <ul> @((MarkupString)Ausgabe) </ul> @code { protected override Task OnInitializedAsync() { return base.OnInitializedAsync(); } public string Ausgabe { get; set; } = ""; async Task Aktion() { // Eine einfache Variable setzen im Browser await JSRuntime.SetValueAsync<Guid>("Token", Guid.NewGuid()); var token = await JSRuntime.GetValueAsync<string>("Token"); Ausgabe = "<li>Aktuelles Token: " + token + "</li>"; // Eine Eigenschaft des vorhandenen document-Objekts ändern var titleOld = await JSRuntime.GetValueAsync<string>("document.title"); var titleNew = "Interop-Demo: " + DateTime.Now.ToLongTimeString(); await JSRuntime.SetValueAsync<string>("document.title", titleNew); Ausgabe += "<li>Title geändert von '" + titleOld + "' auf '" + titleNew + "'" + "</li>"; // Ein neues JavaScript-Objekt erstellen und damit arbeiten var jsObj = await JSRuntime.InvokeNewAsync("JSUtil", "Localhost"); var text = await jsObj.GetValueAsync<string>("text"); var textLength = await jsObj.InvokeAsync<int>("getTextLength"); await jsObj.SetValueAsync<bool>("caseSensitive", true); var caseSensitive = await jsObj.GetValueAsync<bool>("caseSensitive"); var containsTextInUrl = await jsObj.InvokeAsync<bool>("containsTextInUrl"); Ausgabe += $"<li>Die Zeichenkette {text} hat {textLength} Zeichen und kommt{(containsTextInUrl ? "" : " NICHT")} in der aktuellen URL vor. Der Vergleich war '{(caseSensitive ? "casesensitive" : "caseinsensitive")}'.</li>"; } }

Abb. 16: Ausgabe des Beispiels der Listing 15 und 16
Blazor: automatisches Schließen des Filterkriteriendialogs im QuickGrid via HideColumnOptionsAsync()
Die in ASP.NET Core 10.0 Preview 2 eingeführte Methode CloseColumnOptionsAsync() heißt seit Preview 4 nun HideColumnOptionsAsync(). Damit kann man den Eingabedialog für Filterkriterien im Steuerelement <QuickGrid> schließen, wenn der Filter angewendet wurde (Listing 17).
<QuickGrid @ref="flightGrid" RowClass="ApplyRowClass" ItemsProvider="@itemsProvider" TGridItem="BO.WWWings.Flight" Virtualize="true" OverscanCount="@overscanCount"> <PropertyColumn Property="@(p => p.FlightNo)" Title="FlugNr" Sortable="true" /> <PropertyColumn Property="@(p => p.Departure)" Title="Abflugort" Sortable="true"> <ColumnOptions> <input type="search" @bind="departureFilter" placeholder="Filter by Departure" @bind:after="@(() => flightGrid.HideColumnOptionsAsync())" /> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="@(p => p.Destination)" Title="Zielort" Sortable="true" /> <PropertyColumn Property="@(p => p.FlightDate)" Title="Datum" Format="dd.MM.yyyy" Sortable="true" /> </QuickGrid>
Blazor: Diagnosedaten für Blazor WebAssembly
Eine größere neue Diagnosefunktion in .NET 10.0 bekam in Preview 4 die WebAssembly-basierte Variante von Blazor, die als Single Page App im Webbrowser läuft: Entwicklerinnen und Entwickler können im Webbrowser zur Laufzeit Diagnosedaten über Performance, Speicherinhalt und diverse Metriken sammeln.
Dazu muss man als Voraussetzung die .NET WebAssembly Build Tools in der aktuellen .NET-10.0-Version als .NET-SDK-Workload installieren (Abb. 17):
dotnet workload install wasm-tools

Abb. 17: Bei dotnet workload list sollte die jeweils aktuelle Version erscheinen (hier im Bild: Preview 4 von .NET 10.0)
In den .NET WebAssembly Build Tools gibt es zwei neue Funktionen:
- Performanceanalyse in den Browser-Entwicklerwerkzeugen („F12-Tools“)
- Sammeln und Herunterladen von Tracedateien und Memory Dumps aus dem Browser
Für die Nutzung der Performanceanalyse in den Browser-Entwicklerwerkzeugen gibt es in der Projektdatei die in Tabelle 1 aufgelisteten Einstellungen (Listing 18).

Tabelle 1: Projekteinstellungen für Blazor-WebAssembly-Diagnosedaten (Quelle: [17])
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> ... <!-- NEU: Blazor WebAssembly Runtime Diagnostics --> <PropertyGroup> <!-- Für Anzeige von "Timings" im Performance-Tab der Browser-Developer-Tools--> <WasmProfilers>browser</WasmProfilers> <WasmNativeStrip>false</WasmNativeStrip> <WasmNativeDebugSymbols>true</WasmNativeDebugSymbols> </PropertyGroup> </Project>
Mit diesen Einstellungen erhält man in den Browser-Entwicklerwerkzeugen in der Registerkarte Performance eine Ansicht Timings mit der Ablauffolge der aufgerufenen .NET-Methoden (Abb. 18).

Abb. 18: Aufrufhierarchie der Methoden im Browser mit Zeitverbrauch in den Browser-Entwicklerwerkzeugen unter „Performance | Timings“
Mit den folgenden Einstellungen in der Projektdatei erlaubt man das Sammeln von Tracedaten und Memory Dumps einer Blazor-WebAssembly-Anwendung im Webbrowser. Die Daten werden als .nettrace-Datei im Browser heruntergeladen (Tabelle 2, Listing 19).

Tabelle 2: Projekteinstellungen für Blazor-WebAssembly-Diagnosedaten (Quelle: [17])
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> ... <!-- NEU: Blazor WebAssembly Runtime Diagnostics --> <PropertyGroup> <!-- Für Trace- und Memory-Dump-Files --> <WasmPerfTracing>true</WasmPerfTracing> <WasmPerfInstrumentation>all</WasmPerfInstrumentation> <EventSourceSupport>true</EventSourceSupport> <MetricsSupport>true</MetricsSupport> </PropertyGroup> </Project>
Danach kann man per JavaScript APIs auf die Diagnosedaten zugreifen. Diese Befehle gibt man in der Browserkonsole in den Browser-Entwicklerwerkzeugen ein:
- CPU-Nutzung sammeln für die nächsten 5 Sekunden:
globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 5});
- Metriken sammeln für die nächsten 5 Sekunden:
globalThis.getDotnetRuntime(0).collectPerfCounters({durationSeconds: 5});
- Speicherabbildung erstellen:
globalThis.getDotnetRuntime(0).collectGcDump();
Entwicklerinnen und Entwickler erhalten die gesammelten Daten in einer .nettrace-Datei im Downloads-Ordner des Webbrowsers (Abb. 19).

Abb. 19: Herunterladen der Memory-Dump-Datei in Format .nettrace
Die .nettrace-Dateien mit CPU- und Performance-Counter-Dateien kann man ohne weitere Schritte in Visual Studio öffnen, indem man die Datei per File | Open öffnet oder einfach per Drag and Drop in den Dateibereich von Visual Studio zieht (Abb. 20 und 21).

Abb. 20: Ansicht der CPU-Nutzung der Blazor-WebAssembly-Anwendung in Visual Studio

Abb. 21: Nach Klick auf System.String.Join() sieht man die Anzahl der Aufrufe und die CPU-Nutzungszeit
Während man .netrace-Dateien mit CPU- und Performance-Counter-Dateien direkt in Visual Studio öffnen kann, muss man eine Speicherabbilddatei zunächst mit dem .NET-CLI-Werkzeug dotnet-gcdump [18] in eine .gcdump-Datei für Visual Studio konvertieren (Abb. 22).
Die Installation der aktuellen Version dieses Werkzeugs erfolgt mit
dotnet tool install --global dotnet-gcdump
Die Konvertierung einer .nettrace-Datei in eine .gcdump-Datei geht dann so:
dotnet-gcdump convert Dateiname.nettrace

Abb. 22: Konvertieren einer Speicherabbilddatei von .nettrace zu .gcdump
Die erzeugte .gcdump-Datei kann man dann in Visual Studio betrachten (Abb. 23). Man sieht, welche Typen wie oft instanziiert wurden und wie viele Bytes sie verbrauchen. Die Spalte Size (Bytes) sind dabei die Bytes, die der Typ selbst verbraucht. inclusive Size (Bytes) umfasst auch die von Unterobjekten verbrauchten Speicherplätze.

Abb. 23: Betrachten der Memory-Dump-Datei (.gcdump) in Visual Studio
Weitere Neuerungen
Über die in diesem Beitrag besprochenen Neuerungen hinaus bietet .NET 10.0 einige kleinere Verbesserung für Validation Context, Telemetry und ML.NET, die man in den Release Notes zu .NET 10.0 Preview 3 findet [19].
Bei Windows Forms und WPF verwendet Microsoft seit .NET 10.0 Preview 4 den gleichen Programmcode für die Arbeit mit der Zwischenablage [20]. Das erleichtert Microsoft die Wartung des Programmcodes; Entwicklerinnen und Entwickler haben davon aber erstmal nichts.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Ausblick
Bis zum geplanten Erscheinen von .NET 10.0 im November 2025 werden noch drei weitere Preview-Versionen (Preview 5, 6 und 7) und zwei Release-Candidate-Versionen erscheinen. Mit der fertigen Version kann am 11. November 2025 gerechnet werden, denn Microsoft hat bereits eine virtuelle .NET Conf für den 11. bis 13. November 2025 angekündigt.
Als gerade Versionsnummer wird .NET 10.0 wieder Long Term Support (LTS) für 36 Monate erhalten, also bis November 2028, während der Support für .NET 9.0 schon im Mai 2027 enden wird. Weiterhin ist aktuell .NET 8.0 im Support, das Microsoft noch bis November 2026 unterstützen wird.
Links & Literatur
[1] https://dotnet.microsoft.com/en-us/download/dotnet/10.0
[2] https://github.com/oleg-shilo/cs-script
[3] https://github.com/dotnet-script/dotnet-script
[5] https://www.it-visions.de/scripting/dotnetscripting/dsh
[7] https://dotnet-lexikon.de/SystemTextJson/Lex/10722
[11] https://learn.microsoft.com/en-us/azure/cosmos-db/gen-ai/hybrid-search
[16] https://learn.microsoft.com/en-us/dotnet/core/compatibility/10.0
[18] https://learn.microsoft.com/de-de/dotnet/core/diagnostics/dotnet-gcdump
[20] https://github.com/dotnet/winforms/issues/12179